/****************************************************************************
 * Copyright (c) 2007, 2009 Composent, Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Composent, Inc. - initial API and implementation
 *    Mustafa K. Isik - conflict resolution via operational transformations
 *    Marcelo Mayworm - Adding sync API dependence
 *    IBM Corporation - support for certain non-text editors
 *****************************************************************************/

package org.eclipse.ecf.docshare2;

import java.util.Collections;
import java.util.Map;
import org.eclipse.core.filebuffers.ISynchronizationContext;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.Assert;
import org.eclipse.ecf.core.identity.ID;
import org.eclipse.ecf.core.util.ECFException;
import org.eclipse.ecf.core.util.Trace;
import org.eclipse.ecf.datashare.IChannel;
import org.eclipse.ecf.docshare2.messages.FileSystemDocumentChangeMessage;
import org.eclipse.ecf.docshare2.messages.SelectionMessage;
import org.eclipse.ecf.internal.docshare2.DocShareActivator;
import org.eclipse.ecf.internal.docshare2.Messages;
import org.eclipse.ecf.sync.*;
import org.eclipse.ecf.sync.doc.DocumentChangeMessage;
import org.eclipse.ecf.sync.doc.IDocumentSynchronizationStrategyFactory;
import org.eclipse.jface.text.*;
import org.eclipse.jface.text.source.*;
import org.eclipse.osgi.util.NLS;

public class DocumentShare {

	public static class SelectionReceiver {

		private static final String SELECTION_ANNOTATION_ID = "org.eclipse.ecf.docshare2.annotations.RemoteSelection"; //$NON-NLS-1$
		private static final String CURSOR_ANNOTATION_ID = "org.eclipse.ecf.docshare2.annotations.RemoteCursor"; //$NON-NLS-1$

		/**
		 * Annotation model of current document
		 */
		private IAnnotationModel annotationModel;

		/**
		 * Object to use as lock for changing in annotation model,
		 * <code>null</code> if no model is provided.
		 */
		private Object annotationModelLock;

		/**
		 * Annotation for remote selection in annotationModel
		 */
		private Annotation currentAnnotation;

		public void setAnnotationModel(IAnnotationModel annotationModel) {
			this.annotationModel = annotationModel;
			if (this.annotationModel != null) {
				if (this.annotationModel instanceof ISynchronizable) {
					this.annotationModelLock = ((ISynchronizable) this.annotationModel).getLockObject();
				}
				if (this.annotationModelLock == null) {
					this.annotationModelLock = this;
				}
			}
		}

		public void handleMessage(SelectionMessage remoteMsg) {
			if (this.annotationModelLock == null) {
				return;
			}

			final Position newPosition = new Position(remoteMsg.getOffset(), remoteMsg.getLength());
			final Annotation newAnnotation = new Annotation(newPosition.getLength() > 0 ? SELECTION_ANNOTATION_ID : CURSOR_ANNOTATION_ID, false, Messages.DocShare_RemoteSelection);
			synchronized (this.annotationModelLock) {
				if (this.annotationModel != null) {
					// initial selection, create new
					if (this.currentAnnotation == null) {
						this.currentAnnotation = newAnnotation;
						this.annotationModel.addAnnotation(newAnnotation, newPosition);
						return;
					}
					// selection not changed, skip
					if (this.currentAnnotation.getType() == newAnnotation.getType()) {
						Position oldPosition = this.annotationModel.getPosition(this.currentAnnotation);
						if (oldPosition == null || newPosition.equals(oldPosition)) {
							return;
						}
					}
					// selection changed, replace annotation
					if (this.annotationModel instanceof IAnnotationModelExtension) {
						Annotation[] oldAnnotations = new Annotation[] {this.currentAnnotation};
						this.currentAnnotation = newAnnotation;
						Map newAnnotations = Collections.singletonMap(newAnnotation, newPosition);
						((IAnnotationModelExtension) this.annotationModel).replaceAnnotations(oldAnnotations, newAnnotations);
					} else {
						this.annotationModel.removeAnnotation(this.currentAnnotation);
						this.annotationModel.addAnnotation(newAnnotation, newPosition);
					}
				}
			}
		}

		public void dispose() {
			if (this.annotationModelLock == null) {
				return;
			}
			synchronized (this.annotationModelLock) {
				if (this.annotationModel != null) {
					if (this.currentAnnotation != null) {
						this.annotationModel.removeAnnotation(this.currentAnnotation);
						this.currentAnnotation = null;
					}
					this.annotationModel = null;
				}
			}
		}

	}

	private SelectionReceiver selectionReceiver;

	private IChannel channel;

	private IDocument document;

	private String path;

	private ID targetID;

	private boolean locked = false;

	private boolean locallyActive = false;

	private boolean remotelyActive = false;

	/**
	 * Strategy for maintaining consistency among session participants'
	 * documents.
	 */
	private IModelSynchronizationStrategy syncStrategy;

	/**
	 * Factory to returns the possible strategies
	 */
	private IDocumentSynchronizationStrategyFactory factory;

	/**
	 * Create a document sharing session instance.
	 */
	public DocumentShare(IChannel channel, ID targetID, String path, IDocument document) {
		Assert.isNotNull(channel);
		this.channel = channel;
		this.targetID = targetID;
		this.path = path;
		this.document = document;
		factory = DocShareActivator.getDefault().getColaSynchronizationStrategyFactory();
		selectionReceiver = new SelectionReceiver();

		document.addDocumentListener(documentListener);

		//SYNC API. Create an instance of the synchronization strategy on the receiver
		syncStrategy = createSynchronizationStrategy(false);
		Assert.isNotNull(syncStrategy);
	}

	/**
	 * Create a document sharing session instance.
	 */
	public DocumentShare(IChannel channel, ID targetID, String path, IDocument document, IAnnotationModel annotationModel) {
		this(channel, targetID, path, document);
		selectionReceiver.setAnnotationModel(annotationModel);
	}

	void lock() {
		locked = true;
	}

	void unlock() {
		locked = false;
	}

	boolean isLocked() {
		return locked;
	}

	/**
	 * The document listener is the listener for changes to the *local* copy of
	 * the IDocument. This listener is responsible for sending document update
	 * messages when notified.
	 */
	IDocumentListener documentListener = new IDocumentListener() {

		public void documentAboutToBeChanged(DocumentEvent event) {
			// nothing to do
		}

		// handling of LOCAL OPERATION application
		public void documentChanged(DocumentEvent event) {
			if (isLocked()) {
				return;
			}

			Trace.trace(DocShareActivator.PLUGIN_ID, NLS.bind("{0}.documentChanged[{1}]", DocumentShare.this, event)); //$NON-NLS-1$

			// SYNC API.  Here is entry point usage of sync API.  When a local document is changed by an editor,
			// this method will be called and the following code executed.  This code registers a DocumentChange
			// with the local syncStrategy instance via syncStrategy.registerLocalChange(IModelChange).
			// Model change messages returned from the registerLocalChange call are then sent (via ECF datashare channel)
			// to remote participant.
			IModelChangeMessage changeMessages[] = registerLocalChange(new DocumentChangeMessage(event.getOffset(), event.getLength(), event.getText()));
			for (int i = 0; i < changeMessages.length; i++) {
				sendMessage(changeMessages[i]);
			}
		}
	};

	void sendMessage(IModelChangeMessage changeMessage) {
		try {
			channel.sendMessage(targetID, new FileSystemDocumentChangeMessage(path, changeMessage).serialize());
		} catch (ECFException e) {
			DocShareActivator.log(new Status(IStatus.ERROR, DocShareActivator.PLUGIN_ID, "Could not send message to " + targetID, e)); //$NON-NLS-1$
		}
	}

	IModelChangeMessage[] registerLocalChange(IModelChange localChange) {
		return syncStrategy.registerLocalChange(localChange);
	}

	void connect(IAnnotationModel annotationModel) {
		selectionReceiver.setAnnotationModel(annotationModel);
	}

	void disconnect() {
		selectionReceiver.dispose();
	}

	void addDocumentListener() {
		document.addDocumentListener(documentListener);
	}

	void removeDocumentListener() {
		document.removeDocumentListener(documentListener);
	}

	void handleSelectionMessage(SelectionMessage message) {
		selectionReceiver.handleMessage(message);
	}

	/**
	 * This method called by the {@link #handleMessage(ID, byte[])} method if
	 * the type of the message received is an update message.
	 * 
	 * @param documentChangeMessage
	 *            the UpdateMessage received.
	 */
	protected synchronized void handleUpdateMessage(ISynchronizationContext context, final DocumentChangeMessage documentChangeMessage) {
		try {
			lock();
			// SYNC API.  Here a document change message has been received from remote via channel,
			// and is now passed to the syncStrategy for transformation.  The returned IModelChange[]
			// are then applied to the local document (after the synchronization strategy as transformed
			// them as necessary).
			IModelChange modelChanges[] = syncStrategy.transformRemoteChange(documentChangeMessage);
			final ModelUpdateException[] exception = new ModelUpdateException[1];
			for (int i = 0; i < modelChanges.length; i++) {
				if (exception[0] != null) {
					DocShareActivator.getDefault().getLog().log(new Status(IStatus.ERROR, DocShareActivator.PLUGIN_ID, IStatus.ERROR, Messages.DocShare_EXCEPTION_RECEIVING_MESSAGE_TITLE, exception[0]));
					return;
				}

				final IModelChange modelChange = modelChanges[i];
				context.run(new Runnable() {
					public void run() {
						// Apply each change to a model.  Clients may use this method
						// to apply the change to a model of appropriate type
						try {
							modelChange.applyToModel(getDocument());
						} catch (ModelUpdateException e) {
							exception[0] = e;
						}
					}
				});
			}
		} finally {
			unlock();
		}
	}

	IDocument getDocument() {
		return document;
	}

	private IModelSynchronizationStrategy createSynchronizationStrategy(boolean isInitiator) {
		//Instantiate the service
		Assert.isNotNull(factory);
		return factory.createDocumentSynchronizationStrategy(channel.getID(), isInitiator);
	}

	/**
	 * Returns <code>true</code> if the document being backed is locally active, that is, it is being actively edited. For example, this could mean that there is an editor open for the backed document. 
	 * @return <code>true</code> if the backed document is being actively modified, <code>false</code> otherwise
	 */
	boolean isLocallyActive() {
		return locallyActive;
	}

	void setLocallyActive(boolean locallyActive) {
		this.locallyActive = locallyActive;
	}

	/**
	 * Returns <code>true</code> if the document being backed is remotely active, that is, it is being actively edited by a remote peer. For example, this could mean that a peer has an editor open up for the backed document. 
	 * @return <code>true</code> if the backed document is being actively modified remotely, <code>false</code> otherwise
	 */
	boolean isRemotelyActive() {
		return remotelyActive;
	}

	void setRemotelyActive(boolean remotelyActive) {
		this.remotelyActive = remotelyActive;
	}

	public String toString() {
		StringBuffer buf = new StringBuffer("DocumentShare["); //$NON-NLS-1$
		buf.append("path=").append(path).append(";channel=").append(channel); //$NON-NLS-1$ //$NON-NLS-2$
		buf.append("targetID=").append(targetID); //$NON-NLS-1$
		buf.append(";strategy=").append(syncStrategy).append(']'); //$NON-NLS-1$
		return buf.toString();
	}
}
